# window.py
#
# Copyright 2025 mirkobrombin <brombin94@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, in version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import contextlib
import os
import webbrowser
from datetime import datetime, timedelta
from gettext import gettext as _
from typing import Optional

from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Xdp

from bottles.backend.globals import Paths
from bottles.backend.health import HealthChecker
from bottles.backend.logger import Logger
from bottles.backend.managers.data import DataManager, UserDataKeys
from bottles.backend.managers.journal import JournalManager
from bottles.backend.managers.manager import Manager
from bottles.backend.models.config import BottleConfig
from bottles.backend.models.result import Result
from bottles.backend.state import Notification, SignalManager, Signals
from bottles.backend.utils.connection import ConnectionUtils
from bottles.backend.utils.threading import RunAsync
from bottles.frontend.operation import TaskSyncer
from bottles.frontend.params import APP_ID, BASE_ID, PROFILE
from bottles.frontend.utils.gtk import GtkUtils
from bottles.frontend.views.details import DetailsView
from bottles.frontend.views.importer import ImporterView
from bottles.frontend.views.library import LibraryView
from bottles.frontend.views.list import BottleView
from bottles.frontend.views.loading import LoadingView
from bottles.frontend.views.new_bottle_dialog import BottlesNewBottleDialog
from bottles.frontend.views.preferences import PreferencesWindow
from bottles.frontend.windows.crash import CrashReportDialog
from bottles.frontend.windows.depscheck import DependenciesCheckDialog
from bottles.frontend.windows.onboard import OnboardDialog

logging = Logger()


@Gtk.Template(resource_path="/com/usebottles/bottles/window.ui")
class BottlesWindow(Adw.ApplicationWindow):
    __gtype_name__ = "BottlesWindow"

    # region Widgets
    stack_main = Gtk.Template.Child()
    btn_add = Gtk.Template.Child()
    btn_search = Gtk.Template.Child()
    btn_donate = Gtk.Template.Child()
    btn_noconnection = Gtk.Template.Child()
    box_actions = Gtk.Template.Child()
    headerbar = Gtk.Template.Child()
    view_switcher_title = Gtk.Template.Child()
    view_switcher_bar = Gtk.Template.Child()
    main_leaf = Gtk.Template.Child()
    toasts = Gtk.Template.Child()
    # endregion

    # Common variables
    previous_page = ""
    settings = Gio.Settings.new(BASE_ID)
    argument_executed = False

    def __init__(self, arg_bottle, **kwargs):
        width = self.settings.get_int("window-width")
        height = self.settings.get_int("window-height")

        super().__init__(**kwargs, default_width=width, default_height=height)

        self.data_mgr = DataManager()
        first_event = JournalManager.first_event_date()
        days_old = 0
        if first_event:
            days_old = (datetime.now() - first_event).days

        self._show_funding = days_old >= 7 and not self.data_mgr.get(
            UserDataKeys.FundingDismissed
        )

        self.utils_conn = ConnectionUtils(
            force_offline=self.settings.get_boolean("force-offline")
        )
        self.manager = None
        self.arg_bottle = arg_bottle
        self.app = kwargs.get("application")
        self.set_icon_name(APP_ID)

        if PROFILE == "development":
            self.add_css_class("devel")

        self.btn_donate.add_css_class("donate")
        self.__schedule_donate_icon_swap()

        # Set night theme according to user settings
        if self.settings.get_boolean("dark-theme"):
            manager = Adw.StyleManager.get_default()
            manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)

        # Be VERY explicit that non-sandboxed environments are unsupported
        if not Xdp.Portal.running_under_sandbox():

            def response(dialog, response, *args):
                if response == "close":
                    quit(1)

            body = _(
                "Bottles is only supported within a sandboxed environment. Official sources of Bottles are available at"
            )
            download_url = "usebottles.com/download"

            error_dialog = Adw.AlertDialog.new(
                _("Unsupported Environment"),
                f"{body} <a href='https://{download_url}' title='https://{download_url}'>{download_url}.</a>",
            )

            error_dialog.add_response("close", _("Close"))
            error_dialog.set_body_use_markup(True)
            error_dialog.connect("response", response)
            error_dialog.present(self)
            logging.error(
                _(
                    "Bottles is only supported within a sandboxed format. Official sources of Bottles are available at:"
                )
            )
            logging.error("https://usebottles.com/download/")
            return

        # Loading view
        self.page_loading = LoadingView()

        # Populate stack
        self.stack_main.add_named(
            child=self.page_loading, name="page_loading"
        ).set_visible(False)
        self.headerbar.add_css_class("flat")

        # Signal connections
        self.btn_donate.connect(
            "clicked",
            self.open_url,
            "https://usebottles.com/funding/",
        )
        self.btn_add.connect("clicked", self.show_add_view)
        self.btn_noconnection.connect("clicked", self.check_for_connection)
        self.stack_main.connect("notify::visible-child", self.__on_page_changed)

        # backend signal handlers
        self.task_syncer = TaskSyncer(self)
        SignalManager.connect(Signals.TaskAdded, self.task_syncer.task_added_handler)
        SignalManager.connect(
            Signals.TaskRemoved, self.task_syncer.task_removed_handler
        )
        SignalManager.connect(
            Signals.TaskUpdated, self.task_syncer.task_updated_handler
        )
        SignalManager.connect(
            Signals.NetworkStatusChanged, self.network_changed_handler
        )
        SignalManager.connect(Signals.GNotification, self.g_notification_handler)
        SignalManager.connect(Signals.GShowUri, self.g_show_uri_handler)

        self.__on_start()
        logging.info(
            "Bottles Started!",
        )
        GLib.idle_add(self.__maybe_show_funding_dialog)

    def __schedule_donate_icon_swap(self):
        GLib.timeout_add_seconds(5, self.__on_donate_icon_timeout)

    def __on_donate_icon_timeout(self):
        icon_name = self.__resolve_donate_icon_name()
        if icon_name:
            self.btn_donate.set_label("")
            self.btn_donate.set_icon_name(icon_name)
        return GLib.SOURCE_REMOVE

    def __resolve_donate_icon_name(self) -> Optional[str]:
        display = self.get_display()
        icon_theme = None
        if display is not None:
            icon_theme = Gtk.IconTheme.get_for_display(display)
        if icon_theme is None:
            icon_theme = Gtk.IconTheme.get_default()
        if icon_theme is None:
            return None

        try:
            icon_theme.add_resource_path("/com/usebottles/bottles/icons")
        except AttributeError:
            pass

        for icon_name in (
            "heart-symbolic",
            "love-symbolic",
            "emblem-favorite-symbolic",
        ):
            if icon_theme.has_icon(icon_name):
                return icon_name
        return None

    @Gtk.Template.Callback()
    def on_close_request(self, *args):
        self.settings.set_int("window-width", self.get_width())
        self.settings.set_int("window-height", self.get_height())

    # region Backend signal handlers
    def network_changed_handler(self, res: Result):
        GLib.idle_add(self.btn_noconnection.set_visible, not res.status)

    def g_notification_handler(self, res: Result):
        """handle backend notification request"""
        notify: Notification = res.data
        self.send_notification(title=notify.title, text=notify.text, image=notify.image)

    def g_show_uri_handler(self, res: Result):
        """handle backend show_uri request"""
        uri: str = res.data
        Gtk.show_uri(self, uri, Gdk.CURRENT_TIME)

    # endregion

    def update_library(self):
        GLib.idle_add(self.page_library.update)

    def title(self, title, subtitle: str = ""):
        self.view_switcher_title.set_title(title)
        self.view_switcher_title.set_subtitle(subtitle)

    def check_for_connection(self, status):
        """
        This method checks if the client has an internet connection.
        If true, the manager checks will be performed, unlocking all the
        features locked for no internet connection.
        """
        if self.utils_conn.check_connection():
            self.manager.checks(install_latest=False, first_run=True)

    def __on_start(self):
        """
        This method is called before the window is shown. This check if there
        is at least one local runner installed. If not, the user will be
        prompted with the onboard dialog.
        """

        @GtkUtils.run_in_main_loop
        def set_manager(result: Manager, error=None):
            self.manager = result

            tmp_runners = [
                x for x in self.manager.runners_available if not x.startswith("sys-")
            ]
            if len(tmp_runners) == 0:
                self.show_onboard_view()

            # Pages
            self.page_details = DetailsView(self)
            self.page_list = BottleView(self, arg_bottle=self.arg_bottle)
            self.page_importer = ImporterView(self)
            self.page_library = LibraryView(self)

            self.main_leaf.append(self.page_details)
            self.main_leaf.append(self.page_importer)

            self.main_leaf.get_page(self.page_details).set_navigatable(False)
            self.main_leaf.get_page(self.page_importer).set_navigatable(False)

            self.stack_main.add_titled(
                child=self.page_list, name="page_list", title=_("Bottles")
            ).set_icon_name(f"{APP_ID}-symbolic")
            self.stack_main.add_titled(
                child=self.page_library, name="page_library", title=_("Library")
            ).set_icon_name("library-symbolic")

            self.page_list.search_bar.set_key_capture_widget(self)
            self.btn_search.bind_property(
                "active",
                self.page_list.search_bar,
                "search-mode-enabled",
                GObject.BindingFlags.BIDIRECTIONAL,
            )

            if (
                self.stack_main.get_child_by_name(
                    self.settings.get_string("startup-view")
                )
                is None
            ):
                self.stack_main.set_visible_child_name("page_list")

            self.settings.bind(
                "startup-view",
                self.stack_main,
                "visible-child-name",
                Gio.SettingsBindFlags.DEFAULT,
            )

            self.lock_ui(False)
            self.headerbar.get_style_context().remove_class("flat")

            user_defined_bottles_path = self.manager.data_mgr.get(
                UserDataKeys.CustomBottlesPath
            )
            if user_defined_bottles_path and Paths.bottles != user_defined_bottles_path:
                dialog = Adw.MessageDialog.new(
                    self,
                    _("Custom Bottles Path not Found"),
                    _(
                        "Falling back to default path. No bottles from the given path will be listed."
                    ),
                )
                dialog.add_response("cancel", _("_Dismiss"))
                dialog.present()

        def get_manager():
            if self.utils_conn.check_connection():
                SignalManager.connect(
                    Signals.RepositoryFetched, self.page_loading.add_fetched
                )

            # do not redo connection if aborted connection
            mng = Manager(
                g_settings=self.settings,
                check_connection=self.utils_conn.aborted_connections == 0,
            )
            return mng

        self.show_loading_view()
        RunAsync(get_manager, callback=set_manager)

        self.check_crash_log()

    def send_notification(self, title, text, image="", ignore_user=False):
        """
        This method is used to send a notification to the user using
        Gio.Notification. The notification is sent only if the
        user has enabled it in the settings. It is possible to ignore the
        user settings by passing the argument ignore_user=False.
        """
        if ignore_user or self.settings.get_boolean("notifications"):
            notification = Gio.Notification.new(title)
            notification.set_body(text)
            if image:
                icon = Gio.ThemedIcon.new(image)
                notification.set_icon(icon)

            self.props.application.send_notification(None, notification)

    def go_back(self, *_args):
        self.main_leaf.navigate(direction=Adw.NavigationDirection.BACK)

    def show_details_view(self, widget=False, config: Optional[BottleConfig] = None):
        self.main_leaf.set_visible_child(self.page_details)
        self.page_details.set_config(config or BottleConfig())

    def show_loading_view(self, widget=False):
        self.lock_ui()
        self.stack_main.set_visible_child_name("page_loading")

    def show_onboard_view(self, widget=False):
        onboard_window = OnboardDialog(self)
        onboard_window.present(self)

    def show_add_view(self, widget=False):
        new_bottle_dialog = BottlesNewBottleDialog()
        new_bottle_dialog.present(self)

    def show_list_view(self, widget=False):
        self.stack_main.set_visible_child_name("page_list")

    def show_importer_view(self, widget=False):
        self.main_leaf.set_visible_child(self.page_importer)

    def show_prefs_view(self, widget=False, view=0):
        preferences_window = PreferencesWindow(self)
        preferences_window.present()

    def show_download_preferences_view(self, widget=False):
        self.show_prefs_view(widget, view=1)

    def show_runners_preferences_view(self, widget=False):
        self.show_prefs_view(widget, view=2)

    def check_crash_log(self):
        xdg_data_home = GLib.get_user_data_dir()
        log_path = f"{xdg_data_home}/bottles/crash.log"

        with contextlib.suppress(FileNotFoundError):
            with open(log_path, "r") as log_file:
                crash_log = log_file.readlines()
                os.remove(log_path)

            if crash_log:
                CrashReportDialog(self, crash_log).present()

    def __maybe_show_funding_dialog(self):
        if not self._show_funding:
            return

        dialog = Adw.MessageDialog.new(
            self,
            _("Support Bottles"),
            _(
                "With over 3 million installations, Bottles is built by and for its community."
                "\nA donation today helps secure its future and keep it truly independent."
            ),
        )
        dialog.add_response("donate", _("Donate"))
        dialog.add_response("dismiss", _("Don't Show Again"))
        dialog.set_response_appearance("donate", Adw.ResponseAppearance.SUGGESTED)
        dialog.connect("response", self.__funding_response)
        dialog.present()

    def __funding_response(self, dialog, response):
        if response == "donate":
            self.open_url(None, "https://usebottles.com/funding/")
        self.data_mgr.set(UserDataKeys.FundingDismissed, True)
        dialog.destroy()

    def toggle_selection_mode(self, status: bool = True):
        context = self.headerbar.get_style_context()
        if status:
            context.add_class("selection-mode")
        else:
            context.remove_class("selection-mode")

    def lock_ui(self, status: bool = True):
        widgets = [
            self.btn_add,
            self.view_switcher_title,
        ]
        if self.btn_noconnection.get_visible():
            widgets.append(self.btn_noconnection)
        for w in widgets:
            w.set_visible(not status)

    def show_toast(
        self,
        message,
        timeout=3,
        action_label=None,
        action_callback=None,
        dismissed_callback=None,
    ) -> None:
        toast = Adw.Toast.new(message)
        toast.props.timeout = timeout

        if action_label and action_callback:
            toast.set_button_label(action_label)

            def wrapper_callback(*args):
                action_callback(toast)
                toast.handler_block_by_func(dismissed_callback)

            toast.connect("button-clicked", wrapper_callback)

        if dismissed_callback:
            toast.connect("dismissed", dismissed_callback)

        self.toasts.add_toast(toast)

    def __on_page_changed(self, stack, *args):
        is_bottles_list = stack.get_visible_child_name() == "page_list"
        self.btn_search.set_visible(is_bottles_list)

    @staticmethod
    def proper_close():
        """Properly close Bottles"""
        quit()

    @staticmethod
    def open_url(widget, url):
        webbrowser.open_new_tab(url)
